/*
 * IdentityComonentDefinition.java
 *
 * Created on September 2, 2006, 4:37 PM
 *
 * (C) 2006 R. Alexander Milowski
 */

package org.infoset.component.library;

import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.infoset.component.ItemFilterComponentDefinition;
import org.infoset.xml.DocumentLoader;
import org.infoset.xml.Element;
import org.infoset.xml.Infoset;
import org.infoset.xml.InfosetFactory;
import org.infoset.xml.Item;
import org.infoset.xml.ItemConstructor;
import org.infoset.xml.ItemDestination;
import org.infoset.xml.Name;
import org.infoset.xml.Serialization;
import org.infoset.xml.XMLException;
import org.infoset.xml.filter.ItemFilter;
import org.infoset.xml.filter.RemoveDocumentFilter;
import org.infoset.xml.sax.SAXDocumentLoader;
import org.infoset.xml.util.Serializer;
import org.infoset.xml.util.WriterItemDestination;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 *
 * @author alex
 */
public class URLComponentDefinition extends ItemFilterComponentDefinition {
   
   static Logger LOG = Logger.getLogger(URLComponentDefinition.class.getName());
   static final int XML = 0;
   static final int TEXT = 1;
   static final int HTML = 2;
   static final int BINARY = 3;
   
   public static URI NAMESPACE = URI.create("http://www.w3.org/2006/XProc/Components");
   public static Name URL_ACTION = Name.create(NAMESPACE,"url-action");
   public static Name URL_PARAMETER = Name.create(NAMESPACE,"parameter");
   public static Name ERROR = Name.create(NAMESPACE,"error");
   public static Name SUCCESS = Name.create(NAMESPACE,"success");
   public static Name DATA = Name.create(NAMESPACE,"data");
   
   
   static class Identity {
      String username;
      String password;
      String auth;

      Identity(String username, String password) {
         this.username = username;
         this.password = password;
         this.auth = Base64Coder.encode(username+':'+password);
      }
      Identity(String auth) {
         this.username = null;
         this.password = null;
         this.auth = auth;
      }
      public String getUsername() {
         return username;
      }
      public String getBasicAuth() {
         return auth;
      }
   }
   
   static class TagSoupXMLReaderInstantiator implements SAXDocumentLoader.XMLReaderInstantiator {
      public XMLReader newInstance()
         throws SAXException
      {
         return XMLReaderFactory.createXMLReader("org.ccil.cowan.tagsoup.Parser");
      }
   }
   
   
   public static class URLFilter implements ItemFilter {
      
      class ParameterWriterDestination implements ItemDestination {
         StringBuilder builder = new StringBuilder();
         boolean started = false;
         public void send(Item item)
            throws XMLException {
             switch (item.getType()) {
                case ElementItem:
                    Element e = (Element)item;
                    if (e.getName().equals(URL_PARAMETER)) {
                       String name = e.getAttributeValue("name");
                       String value = e.getAttributeValue("value");
                       if (started) {
                           builder.append('&');
                       }
                       builder.append(name);
                       builder.append('=');
                       builder.append(URLEncoder.encode(value));
                       started = true;
                    }
                    break;
                 case DocumentEndItem:

                    try {
                       OutputStream os = connection.getOutputStream();
                       String encoded = builder.toString();
                       DataOutputStream dout = new DataOutputStream(os);
                       dout.writeBytes(encoded);
                       dout.flush();
                       dout.close();
                       //System.err.println("Writing: "+encoded);
                    } catch (java.io.IOException ex) {
                        throw new XMLException("Cannot write encoded parameters.",ex);
                    }
             }
         }
      }
      
      boolean statusOnly;
      int dumpLevel = -1;
      int writeLevel = -1;
      URL target;
      ItemDestination writeDestination;
      URLConnection connection;
      Infoset currentInfoset;
      boolean closeOnError;
      String errorMessage;
      Identity id;
      ItemDestination output = null;
      String requestContentType;
      String requestCharset;
      String responseContentType;
      String responseCharset;
      
      protected void handleSend(boolean isPost,Identity requestId) 
         throws XMLException
      {
         try {
            connection = target.openConnection();
            ((HttpURLConnection)connection).setRequestMethod(isPost ? "POST" : "PUT");
            connection.setDoInput(true);
            connection.setDoOutput(true);
            if (requestId!=null) {
               connection.setRequestProperty("Authorization", "Basic " + requestId.getBasicAuth());
            } else if (id!=null) {
               connection.setRequestProperty("Authorization", "Basic " + id.getBasicAuth());
            }
            if ("application/x-www-form-urlencoded".equals(requestContentType)) {
               connection.setRequestProperty("Content-Type", requestContentType);
               writeDestination = new ParameterWriterDestination();
               writeDestination.send(currentInfoset.createItemConstructor().createDocument());
               writeLevel = 0;
            } else {
               if (requestCharset==null) {
                  requestCharset = "UTF-8";
               }
               connection.setRequestProperty("Content-Type", (requestContentType==null ? "text/xml" : requestContentType)+"; charset="+requestCharset);
               writeDestination = new WriterItemDestination(new OutputStreamWriter(connection.getOutputStream(),requestCharset),requestCharset,true);
               writeDestination.send(currentInfoset.createItemConstructor().createDocument());
               ((Serialization)writeDestination).start();
               writeLevel = 0;
            }
         } catch (MalformedURLException ex) {
            error(currentInfoset.createItemConstructor(),-1, "Bad URL: "+target+", error "+ex.getMessage());
            dumpLevel = 0;
         } catch (IOException ex) {
            error(currentInfoset.createItemConstructor(),-1, "I/O error while establishing connection"+ex.getMessage());
            dumpLevel = 0;
         }
      }
      
      protected void handleGet(boolean isDelete,boolean isHead,Identity requestId) 
         throws XMLException
      {
         RemoveDocumentFilter dest = new RemoveDocumentFilter(output);
         InputStream is = null;
         try {
            URLConnection connection = target.openConnection();
            if (isDelete) {
                ((HttpURLConnection)connection).setRequestMethod("DELETE");
            } else if (isHead) {
                ((HttpURLConnection)connection).setRequestMethod("HEAD");
            }
            if (requestId!=null) {
               connection.setRequestProperty("Authorization", "Basic " + requestId.getBasicAuth());
            } else if (id!=null) {
               connection.setRequestProperty("Authorization", "Basic " + id.getBasicAuth());
            }
            is = connection.getInputStream();

            int statusCode = HttpURLConnection.HTTP_OK;

            if (connection instanceof HttpURLConnection) {
               statusCode = ((HttpURLConnection)connection).getResponseCode();
            }
            if (statusCode==HttpURLConnection.HTTP_OK) {
               if (isDelete || isHead || statusOnly) {
                  ItemConstructor constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
                  success(constructor, statusCode, isDelete ? "Resource deleted." : "Resource found.");
               } else {
                   String contentType = connection.getContentType();
                   handleResponse(contentType,is,dest);
               }
            } else {
               ItemConstructor constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
               error(constructor, statusCode,slurp(is));
            }
         } catch (FileNotFoundException ex) {
            ItemConstructor constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
            error(constructor, HttpURLConnection.HTTP_NOT_FOUND,ex.getMessage());
         } catch (Exception ex) {
             if (closeOnError && dest.getLevel()>=0) {
                ItemConstructor constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
                dest.primeItemConstructor(constructor);
                dest.closeElementsToLevel(constructor, 1);
                dest.send(constructor.createElement(ERROR));
                dest.send(constructor.createCharacters(errorMessage==null ? ex.getMessage() : errorMessage));
                dest.send(constructor.createElementEnd(ERROR));
                dest.closeElements(constructor);
             } else if (ex instanceof XMLException) {
                throw (XMLException)ex;
             } else {
                throw new XMLException("I/O error during URL get/parse.",ex);
             }
         } finally {
            if (is!=null) {
               try {
                  is.close();
               } catch (IOException ex) {
                  LOG.log(Level.SEVERE,"Cannot close connection.",ex);
               }
            }
         }
      }
      
      void handleResponse(String contentType, InputStream is,ItemDestination dest)
         throws IOException, XMLException
      {
          if (responseContentType!=null) {
             contentType = responseContentType;
          } 
          String charset = "UTF-8";
          int kind = XML;
          if (contentType!=null) {
             int semicolon = contentType.indexOf(';');
             if (semicolon>=0) {
                String type = contentType.substring(0,semicolon);
                String rest = contentType.substring(semicolon+1);
                int equals = rest.indexOf('=');
                if (equals>=0 && rest.substring(0,equals).equals("charset")) {
                   charset = rest.substring(equals+1);
                }
                contentType = type;
             }
          }
          if (contentType.equals("text/xml") || contentType.equals("application/xml")) {
             kind = XML;
          } else if (contentType.startsWith("application/xhtml")) {
             kind = XML;
          } else if (contentType.endsWith("+xml")) {
             kind = XML;
          } else if (contentType.startsWith("text/html")) {
             kind = HTML;
          } else if (contentType.equals("text/plain")) {
             kind = TEXT;
          } else {
             kind = BINARY;
          }
          if (responseCharset!=null) {
             charset = responseCharset;
          }

          DocumentLoader docLoader;

          if (kind==XML) {
             docLoader = new SAXDocumentLoader();
             try {
                docLoader.generate(
                   charset==null ? new InputStreamReader(is) : 
                                    new InputStreamReader(is,charset),
                   target.toURI(),
                   dest
                );
             } catch (URISyntaxException ex) {
                throw new XMLException("Cannot convert URL back to URI.",ex);
             }
          } else if (kind==HTML) {
             docLoader = new SAXDocumentLoader(new TagSoupXMLReaderInstantiator());
             try {
                docLoader.generate(
                   charset==null ? new InputStreamReader(is) : 
                                    new InputStreamReader(is,charset),
                   target.toURI(),
                   dest
                );
             } catch (URISyntaxException ex) {
                throw new XMLException("Cannot convert URL back to URI.",ex);
             }
          } else if (kind==TEXT) {
             Reader r = charset==null ? new InputStreamReader(is) : 
                                        new InputStreamReader(is,charset);
             StringBuilder builder = new StringBuilder();
             char [] buffer = new char[8192];
             int len;
             while ((len=r.read(buffer))>=0) {
                builder.append(buffer,0,len);
             } 
             ItemConstructor constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
             Element data = constructor.createElement(DATA);
             data.setAttributeValue("content-type",contentType);
             dest.send(data);
             dest.send(constructor.createCharacters(builder.toString()));
             dest.send(constructor.createElementEnd(DATA));
          } else if (kind==BINARY) {
             byte [] buffer = new byte[8192];
             StringBuilder builder = new StringBuilder();
             int len;
             while ((len=is.read(buffer))>=0) {
                builder.append(Base64Coder.encode(buffer,0,len));
             } 
             ItemConstructor constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
             Element data = constructor.createElement(DATA);
             data.setAttributeValue("content-type",contentType);
             dest.send(data);
             dest.send(constructor.createCharacters(builder.toString()));
             dest.send(constructor.createElementEnd(DATA));
          }

         
      }
      
      public void send(Item item) throws XMLException
      {
         if (dumpLevel>=0) {
            switch (item.getType()) {
               case ElementItem:
                  dumpLevel++;
                  break;
               case ElementEndItem:
                  dumpLevel--;
            }
            return;
         }
         if (writeLevel>=0) {
            switch (item.getType()) {
               case ElementItem:
                  writeLevel++;
                  break;
               case ElementEndItem:
                  writeLevel--;
            }
            if (writeLevel<0) {
               ItemConstructor iconstructor = item.getInfoset().createItemConstructor();
               writeDestination.send(iconstructor.createDocumentEnd());
               if (writeDestination instanceof Serializer) {
                   ((Serializer)writeDestination).end();
               }
               InputStream is = null;
               try {
                  connection.getOutputStream().close();
                  is = connection.getInputStream();
                  int statusCode = HttpURLConnection.HTTP_OK;
                  if (connection instanceof HttpURLConnection) {
                     statusCode = ((HttpURLConnection)connection).getResponseCode();
                     if (statusCode!=HttpURLConnection.HTTP_OK && statusCode!=HttpURLConnection.HTTP_CREATED) {
                        error(iconstructor,statusCode,slurp(is));
                        try {
                           is.close();
                        } catch (IOException ex) {
                           LOG.log(Level.SEVERE,"Cannot close connection.",ex);
                        }
                        return;
                     }
                  }
                  if (statusOnly) {
                     ItemConstructor constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
                     success(constructor, statusCode, slurp(is));
                  } else {
                     String contentType = connection.getContentType();
                     RemoveDocumentFilter dest = new RemoveDocumentFilter(output);
                     try {
                        handleResponse(contentType,is,dest);
                     } catch (XMLException ex) {
                        if (closeOnError && dest.getLevel()>=0) {
                           ItemConstructor constructor = InfosetFactory.getDefaultInfoset().createItemConstructor();
                           dest.primeItemConstructor(constructor);
                           dest.closeElementsToLevel(constructor, 1);
                           dest.send(constructor.createElement(ERROR));
                           dest.send(constructor.createCharacters(errorMessage==null ? ex.getMessage() : errorMessage));
                           dest.send(constructor.createElementEnd(ERROR));
                           dest.closeElements(constructor);
                        } else {
                           throw ex;
                        }
                     }
                  }
               } catch (IOException ex) {
                  HttpURLConnection hconnection = (HttpURLConnection)connection;
                  String msg = null;
                  InputStream es = null;
                  int statusCode = -1;
                  try {
                     statusCode = hconnection.getResponseCode();
                     es = hconnection.getErrorStream();
                     if (es!=null) {
                        msg = slurp(es);
                     }
                  } catch (IOException inner) {
                  } finally {
                     if (es!=null) {
                        try {
                           es.close();
                        } catch (IOException closeEx) {
                        }
                     }
                  }
                  if (msg==null) {
                     error(iconstructor,-1,"I/O error while parsing response: "+ex.getMessage()+msg);
                  } else {
                     error(iconstructor,statusCode,msg);
                  }
               } finally {
                  if (is!=null) {
                     try {
                        is.close();
                     } catch (IOException ex) {
                        LOG.log(Level.SEVERE,"Cannot close connection.",ex);
                     }
                  }
               }
            } else {
               writeDestination.send(item);
            }
            return;
         }
         if (item.getType()==Item.ItemType.ElementItem) {
            Element e = (Element)item;
            currentInfoset = e.getInfoset();
            Name nm = e.getName();
            if (nm.equals(URL_ACTION)) {
               String verb = e.getAttributeValue("method");
               verb = verb.toLowerCase();
               String href = e.getAttributeValue("href");
               if (href==null) {
                  throw new XMLException("Missing the 'href' attribute on "+URL_ACTION);
               }
               Identity requestId = null;
               String basicAuth = e.getAttributeValue("basic-auth");
               if (basicAuth!=null) {
                  requestId = new Identity(basicAuth);
               } else {
                  String username = e.getAttributeValue("username");
                  if (username!=null && username.length()!=0) {
                     String password = e.getAttributeValue("password");
                     if (password==null) {
                        throw new XMLException("The 'password' attribute is required when the 'username' attribute is specified.");
                     }
                     requestId = new Identity(username,password);
                  }
               }
               try {
                   URI base = e.getBaseURI();
                   if (base!=null) {
                      target = base.resolve(href).toURL();
                   } else {
                      target = new URL(href);
                   }
                   requestContentType = e.getAttributeValue("content-type");
                   requestCharset = e.getAttributeValue("charset");
                   responseContentType = e.getAttributeValue("response-content-type");
                   responseCharset = e.getAttributeValue("response-charset");
                   if (verb.equals("post")) {
                      handleSend(true,requestId);
                   } else if (verb.equals("get")) {
                      handleGet(false, false, requestId);
                      dumpLevel = 0;
                   } else if (verb.equals("head")) {
                      handleGet(false, true ,requestId);
                      dumpLevel = 0;
                   } else if (verb.equals("delete")) {
                      handleGet(true, false,requestId);
                      dumpLevel = 0;
                   } else if (verb.equals("put")) {
                      handleSend(false, requestId);
                   } else {
                      throw new XMLException("Method name '"+verb+"' is not allowed.");
                   }
               } catch (IOException ex) {
                   throw new XMLException("Cannot create connection due to I/O error.",ex);
               }
               return;
            }
         }
         output.send(item);
      }
      
      void error(ItemConstructor constructor,int code,String message) 
         throws XMLException
      {
         Element error = constructor.createElement(ERROR);
         if (code!=(-1)) {
            error.setAttributeValue("status", Integer.toString(code));
         }
         output.send(error);
         output.send(constructor.createCharacters(message));
         output.send(constructor.createElementEnd(ERROR));
      }

      void success(ItemConstructor constructor,int code,String message) 
         throws XMLException
      {
         Element success = constructor.createElement(SUCCESS);
         if (code!=(-1)) {
            success.setAttributeValue("status", Integer.toString(code));
         }
         output.send(success);
         output.send(constructor.createCharacters(message));
         output.send(constructor.createElementEnd(SUCCESS));
      }

      String slurp(InputStream is) 
         throws IOException
      {
         InputStreamReader r = new InputStreamReader(is,"UTF-8");
         StringBuilder builder = new StringBuilder();
         char [] buffer = new char[1024];
         int len = 0;
         while ((len=r.read(buffer))>0) {
            builder.append(buffer,0,len);
         }
         return builder.toString();
      }
      
      public void attach(ItemDestination dest) {
         this.output = dest;
      }
   }
   
   /** Creates a new instance of IdentityComonentDefinition */
   public URLComponentDefinition(Name name,String vendor,String version,URI vendorLocation) {
      super(name,vendor,version,vendorLocation,URLFilter.class);
   }
   
}
